使用Service workers实现离线内容

原文地址:Offline content with service workers

Service workers不只是对网页进行离线支持它还可以做很多事情,但是包括我自己在内对于大多数人而言,这将是他们的第一次使用Service workers经验。我最近给我的博客实现了一个简单的离线页面,令我惊喜的是使用它竟然是如此的简单。(充满着十足的自信)我想我可以做更多的事情。我决定开始保存我的博客文章从而实现离线阅读,并且很快开始投入实践。之后我很快意识到这个“坑”很深。

这并不是在责怪service workers,而这是一个迹象,说明它们是多么的强大和灵活。还好发现的及时,随着概念变得越来越熟悉,以及复杂性逐渐被抽象化,离线内容将变得更加常见。事实上,我喝了Kool-Aid(译者注:一种饮料),并且我可以知道为什么很多人认为,在几年内离线内容将像如今在web开发中的响应式设计一样变的无处不在。

话虽如此,但是在开始之前这里有一些事情我希望我是知道的。

浏览器支持缓存

Service workers表面上看是一个渐进增强的简单备选方案,在注册一个service worker之前检测它的支持性是很简单的。你可以像这样去做:

1
2
3
4
if ('serviceWorker' in navigator) {
// Yay, service workers work!
navigator.serviceWorker.register('/sw.js');
}

看上去似乎足够简单,但是这里有一个问题。如果你看过了MDN page for the service worker cache API,你将会了解到不同版本的Chrome浏览器支持不同的缓存方法。这也就意味着,尽管你认真努力的做了功能的支持性检测,当使用addAll方法的时候介于版本为40和45之间的Chrome将会出现一个错误。当这些版本的浏览器被更加广泛的使用的时候这就是一个问题了。我在写这篇文章的时候已经在Can I Use检测过了,看上去它可能会影响到约1.15%的用户。

在开始service workers之前我已经读了几篇相关的博客和教程。一些人主张仅仅是使用put而不是addAll,其他人则推荐使用cache pollyfill,尽管其他一些人还仍然没有提到它。明显的这些都是在不同的时间段写的,并且为了找出正确的方式我花费了很多的时间去研究它。

最后,有这么少量的用户,并且还将会变的越来越少。我选择用addAll方法来检测那些不支持它的浏览器,就像它们根本不支持service workers一样。

因此,我的特征检测方法变成了这样:

1
2
3
4
if ( 'serviceWorker' in navigator && (typeof Cache !== 'undefined' && Cache.prototype.addAll) ) {
// Yay, this is a problem we didn't need to have!
navigator.serviceWorker.register('/sw.js');
}

这样是有一点啰嗦,但是这里我只是提出了我的方式,仅仅是为了避免控制台输出错误。我已经在所有主流浏览器上测试过了,包括那些不支持addAll方法的关键版本的浏览器。我很开心使用这样的方法,而且它是如此的棒。

service workers用在哪里

当你注册service worker的时候你需要指明一个含有service worker逻辑的JavaScript文件,也就是说,如果你想去跨域实现service workers,你必须将service worker放置在你站点的根路径下。为了安全原因,service workers仅仅是控制和它同一级目录下的页面或者是下一级目录下的。事实上这将意味着这并不是在你的站点下的JavaScript目录,而是作为我的首次尝试。我相信这写得已经像白天一样清楚明了了,除了我,这对每个人来说都是显而易见的。

然而就这个话题,值得一提的是service workers仅仅是通过HTTPS或者本地域名下工作的。对我来说很幸运的是,我的博客已经配置成将HTTP通信重定向到HTTPS。如果你也能这样做的话那将是个不错的主意。如果不可以的话,你需要在注册service worker之前检查你的应用是否在安全的域下。

我们可以使用service worker了吗?

是的,我们现在就准备开始使用service worker!在开始的时候我推荐大家阅读Jake的“The Service Worker”和Archibald的Offline Cookbook。这两篇文章作为入门阅读是很好的地方,里面的链接和参考引用都包含了很有价值的信息。

你将会很快的了解到关于service worker的很多东西,并且会知道哪里需要考虑到离线内容。在一个service worker中我们需要监听3个主要的事件:

  • install
  • activate
  • fetch

当service worker被第一次注册的时候,install事件只会被触发一次。这里我们用基本的资源来设置缓存。我的install事件没有任何特殊的地方而是相当的简单。我缓存了首页,CSS以及一个离线页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var CACHE_NAME = 'v1::madebymike';
var urlsToCache = [
'/',
'/offline.html',
'/css/styles.min.css'
];

// Install
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(CACHE_NAME).then(function(cache) {
return cache.addAll(urlsToCache);
})
);
});

activate事件是在install事件之后,并且是每次你导航到被service worker所管理的域下被触发的。

我的activate事件也是相当的标准写法了,我仅仅给我的service worker使用了一次缓存。这种模式可以检测任何缓存的名称,使得它们可以匹配到CACHE_NAME变量,如果不能匹配到将会被删除。这使我意识到我需要手动去校验我的service worker缓存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
self.addEventListener('activate', function(event) {
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.filter(function(cacheName) {
return cacheName !== CACHE_NAME;
}).map(function(cacheName) {
console.log('Deleting '+ cacheName);
return caches.delete(cacheName);
})
);
})
);
});

最后,fetch事件是每次一个页面被请求都会被触发。不管用户当前是否处于离线状态fetch事件总是会被拦截。就像我早前说过的那样service workers != offline content!离线内容仅仅是service workers的一种实现。Service workers已经有能力去加速日常网页的浏览,这真的是一个好消息啊。

这是我的第一个fetch事件的列子。

1
2
3
4
5
6
7
8
9
10
self.addEventListener('fetch', function(event) {
event.respondWith(
// If network fetch fails serve offline page form cache
fetch(event.request).catch(function(error) {
return caches.open(CACHE_NAME).then(function(cache) {
return cache.match('/offline.html');
});
})
);
});

A better service worker (down the rabbit hole) 更好的service worker(入坑了)

在这个观点上我自己是很高兴的如果你想去实现离线内容,针对上述内容是很好的开始。我需要为了做到离线阅读而去缓存我的博客文章,在可能的地方我需要从缓存中返回页面内容给用户。

为了最终探索实现出这种模式我做了大量的尝试和犯了一些错误。当提供默认缓存页面的时候你需要相当的注意。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
self.addEventListener('fetch', function(event) {

var requestURL = new URL(event.request.url);

event.respondWith(
caches.open(CACHE_NAME).then(function(cache) {
return cache.match(event.request).then(function(response) {

// If there is a cached response return this otherwise grab from network
return response || fetch(event.request).then(function(response) {

// Check if the network request is successful
// don't update the cache with error pages!!
// Also check the request domain matches service worker domain
if (response.ok && requestURL.origin == location.origin){
// All good? Update the cache with the network response
cache.put(event.request, response.clone());
}

return response;

}).catch(function() {

// We can't access the network, return an offline page from the cache
return caches.match('/offline.html');

});

});
});
);
});

这种模式总是试图首先从缓存中提取内容,但是与此同时我开启了一个网络请求。如果网络请求成功返回,我更新缓存中的内容,所以这并不是一个错误的页面。这也意味着当一个用户访问我的网站,它们将看到的是最后一次版本缓存的内容,没必要是最新的版本内容。在随后的访问中或者是一次刷新操作他们将从缓存中检索最新的页面。如果我做了主要的变化比如说CSS,我可以在service worker脚本中改变CACHE_NAME

A better offline page (deeper down the rabbit hole) 更好的离线页面(入了更深的坑了)

通常上的离线页面来自我的第一个fetch例子,当页面内容没有被缓存以及网络请求失败页面仍然能够展示服务于用户,但是我想在这方面做的更多些。如果我们没有他们想的那样展示页面内容,我认为当用户的缓存中有可展示呈现的页面是很有帮助的。因此我再次栽入坑中了。。

这里有一个连系service workers的方法,web工作者称之为channel messaging API

在service worker中我监听message事件,一旦我从缓存中获取到了页面的列表,并且在我的站点上给我的博客文章匹配URL规则然后发送一个响应返回到离线的页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
self.addEventListener('message', function(event) {
caches.open(CACHE_NAME).then(function(cache) {

return cache.keys().then(function(requests) {

var urls = requests.filter(function(request){
return request.url.indexOf("/writing/") !== -1;
}).map(function(request) {
return request.url;
});
return urls.sort();

}).then(function(urls) {
event.ports[0].postMessage(urls);
});

});
});

在我的离线页面上我给service worker发送一个信息并且作为响应信息来监听,但是这样并不是很聪明的做法。这时候我发送的信息是什么已经不重要了,我将会总是收到相同的响应。但是对于现在来说已经是足够的了,我不想将其复杂化。

1
2
3
4
5
6
var messageChannel = new MessageChannel();
messageChannel.port1.onmessage = function(event) {
// Add list of offline pages to body with JavaScript
// `event.data` contains an array of cached URLs
};
navigator.serviceWorker.controller.postMessage("get-pages", [messageChannel.port2]);

我最糟糕的离线经验案例现在看起来像这样:

What next?

当用户正在阅读一些离线内容的时候我将给他们一些指示。我认为这是很有帮助的,在网络状况差的情况下这不会觉得很明显。同时或许也将会使用message API,也或许会研究推送通知。

我希望讲解我实现离线内容的经验能够帮助你使用起service worker来变的更加简单,又或者仅仅是鼓励你开始去使用它。我认为这期间最困难的事情是理解当向所有的用户提供缓存内容服务时所做出的选择的影响。自己要确定你搞清楚这个事情以及花费了一些时间去理解service workers是怎样工作的是很重要的。当然在这方面我不是专家,如果我已经犯了一些错误,请让我知道这样我就可以更新它。

Other stuff I wrote 我写的一些别的东西

坚持原创技术分享,您的支持将鼓励我继续创作!